﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using CoreCalc;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;

namespace SmallVCM
{
    public class Camera
    {
        Vector mPosition;
        Vector mForward;
        UV mResolution;
        Matrix4x4 mRasterToWorld;
        Matrix4x4 mWorldToRaster;
        float mImagePlaneDist;

        public void Setup(
        Vector aPosition,
        Vector aForward,
        Vector aUp,
        UV aResolution,
        float aHorizontalFOV)
        {
            Vector forward = Vector.Normalize(aForward);
            var df = -forward;
            Vector up = Vector.Normalize(Vector.Cross(ref aUp, ref df));
            Vector left = Vector.Cross(ref df, ref up);

            mPosition = aPosition;
            mForward = forward;
            mResolution = aResolution;

            var pos = new Vector(
                Vector.Dot(ref up, ref aPosition),
                Vector.Dot(ref left, ref aPosition),
                Vector.Dot(ref df, ref aPosition));

            var worldToCamera = new Matrix4x4().Identity();
            worldToCamera.SetRow(0, ref up, -pos.x);
            worldToCamera.SetRow(1, ref left, -pos.y);
            worldToCamera.SetRow(2, ref df, -pos.z);

            Matrix4x4 perspective = Matrix4x4.Perspective(aHorizontalFOV, 0.1f, 10000f);
            Matrix4x4 worldToNScreen = perspective * worldToCamera;
            Matrix4x4 nscreenToWorld = Matrix4x4.Inverse(ref worldToNScreen);

            mWorldToRaster =
                Transform.Scale(aResolution.U * 0.5f, aResolution.V * 0.5f, 0).GetMatrix() *
                Transform.Translate(new Vector(1f, 1f, 0)).GetMatrix() * worldToNScreen;

            mRasterToWorld = nscreenToWorld *
                Transform.Translate(new Vector(-1f, -1f, 0)).GetMatrix() *
                Transform.Scale(2f / aResolution.U, 2f / aResolution.V, 0).GetMatrix();

            float tanHalfAngle = (float)Math.Tan(aHorizontalFOV * MathLab.M_PI / 360f);
            mImagePlaneDist = aResolution.U / (2f * tanHalfAngle);
            r2w = new Transform(ref mRasterToWorld);
            w2r = new Transform(ref mWorldToRaster);
        }

        private Transform r2w, w2r;

        public int RasterToIndex(ref UV aPixelCoords)
        {
            return (int)(MathLab.Floor(aPixelCoords.U) + MathLab.Floor(aPixelCoords.V) * mResolution.U);
        }

        public UV IndexToRaster(int aPixelIndex)
        {
            float y = MathLab.Floor(aPixelIndex / mResolution.U);
            float x = (aPixelIndex) - y * mResolution.V;
            return new UV(x, y);
        }

        public Vector RasterToWorld(UV aRasterXY)
        {
            //var p = new Vector(aRasterXY.U, aRasterXY.V, 0.0f);
            var pt = new Point(aRasterXY.U, aRasterXY.V, 0.0f);

            return (r2w & pt).ToVec();
            //return mRasterToWorld.TransformPoint(ref p);


        }

        public UV WorldToRaster(Vector aWorldPos)
        {
            var temp = mWorldToRaster.TransformPoint(ref aWorldPos);
            return new UV(temp.x, temp.y);
        }

        // returns false when raster position is outside screen space
        public bool CheckRaster(ref UV aRasterPos)
        {
            return aRasterPos.U >= 0 && 
                   aRasterPos.V >= 0 &&
                   aRasterPos.U < mResolution.U && 
                   aRasterPos.V < mResolution.V;
        }

        public void GenerateRay(UV aRasterXY, out Point org, out Vector dir)
        {
            var worldRaster = this.RasterToWorld(aRasterXY);

            org = (Point)mPosition + worldRaster*0.1f;
            dir = Vector.Normalize(worldRaster - mPosition);
        }
    }

}
